//
// Created by yanhai on 2020/3/28.
//

#ifndef __COMPLEX_H__
#define __COMPLEX_H__   // 防卫式声明

#include <iostream>
/**
 * 一个好的类的实现原则：
 *   1.数据一定要放在private
 *   2.参数尽可能的使用by reference来传（看实际情况要不要加const）
 *   3.返回值首先考虑使用by reference来传（如果不可以使用引用，那就按值传递）
 *   4.类内本体函数可以加const的一定要加const
 *   5.构造函数尽量使用初始化列表
 */

// 0 前置声明
class complex;

complex &__doapl(complex *ths, const complex &r);

// 1 类声明
class complex  // class head
    // class body
{
    /**
     * 知识点2 访问权限：
     *  2.1
     * public: 供外接使用的接口
     * private: 私有的，类外无法访问
     * public和private可以交错多次出现
     */
public:
    /**
     * 知识点3 构造函数：
     *  3.1 名称 参数 返回值
     *   函数名必须与类名相同
     *   参数 double r = 0, double i = 0  其中 = 0 为默认参数
     *   没有返回值
     *  3.2 初始化列表
     *   冒号后面是 初始化列表，只有构造函数才有的特性，构造函数中赋值尽量使用初始化列表，好处多多
     *   使用初始化列表就相当于定义一个变量时并初始化 int i = 100;
     *   不使用初始化列表，而在构造函数体内进行变量赋值就相当于，int i; i = 100;
     *   效率自然是初始化列表更高
     * @param r
     * @param i
     */
    complex(double r = 0, double i = 0) : re(r),
                                          im(i) {}

    /**
     * 知识点4 函数重载：
     *  4.2
     *   既然同名函数可以写多个，那我写一个下面的函数，结果却编译不通过，为什么？
     *   因为：
     *     当定义下面对象时，两个构造函数都满足调用条件，这是编译器就不知道该调用哪一个了，因此不允许编写下面的无参构造函数
     *       complex c1;
     *       complex c2();  // 这种也是定义一个没有参数的对象，但是编译时会给出告警信息，不推荐这样写，应该写成上面c1那个样子。
     *
     */
//    complex() : re(0), im(0) {}

    /**
     * 知识点7 参数传递: pass by value   vs   pass by reference(to const)
     *        参数传递：按值传递 vs 按引用传递(const)
     *  按值传递：整体打包赋值（实参复制一份给形参）
     *  按引用传递：即 将实参地址传递给形参（引用本质上就是指针，效率上与指针完全相同，但比指针形式上漂亮，好用）
     *  按值传递效率很低（内置类型如int char按值和引用传递效率差不多，怎么写都可以），尽量不要用按值传递
     *  传引用时，为了防止在函数内部改了形参的值而影响了外部实参的值，可以加const修饰形参（加const场景：按引用传递仅仅是为了传递的效率，并不想改变实参的值）。
     * 总结：1.参数传递尽量都使用引用（内置类型无所谓，按值或按引用都可以）；2.按引用传递时，不想在函数调用过程中影响实参的值，形参要加const修饰。
     *
     * 知识点8 返回值传递: return by value   vs   return by reference(to const)
     *        返回值传递：按值传递 vs 按引用传递(const)
     *  尽量返回引用类型：前提是如果可以的话，因为有很多情况是不可以的。
     *  可以的情况：要返回的变量（对象）不是在本函数内部被创建的（static类型变量/对象除外）
     *  不可以的情况：要返回的变量（对象）是在本函数内部被创建的，并且不是static修饰的。
     *
     * @return
     */
    complex &operator+=(const complex &);

    // 有些函数在此直接定义，另一些在body之外定义
    /**
     * 知识点1 inline(内联)函数：
     *   1.1 inline函数只是向编译器发出的一种请求，最终能否成为内联函数由编译器决定
     *   1.2 在.h文件中，类中直接实现（定义）的函数默认即为inline函数，在类声明中声明的函数，同样也在.h中实现（定义）的函数可以在定义处使用inline修饰变为inline函数。
     *   1.3 若在class body只是声明并指定为inline函数，该函数实现必须在当前.h文件中，
     *       如果将该函数的定义放在.cpp文件中，那么只能在该.cpp文件中使用该函数，其他.cpp中无法使用。
     *   1.4 inline函数只在当前文件中生效， inline函数在.h文件中声明及定义时，所有包含该文件的源文件都有一份inline函数定义，所有源文件中的该函数体都相同。
     *   1.5 但是在.h中类声明里声明的函数，在对应的源文件中实现该函数，如果在定义处加inline修饰，该函数只能在该源文件中使用，在其他源文件（源文件包含了这个.h）中使用该函数会出现符号未定义的编译错误。
     *   inline函数的本质：每个源文件中都有一份函数实现，并且会在被调用处展开。
     * @return
     */
    double real() const { return re; }

    /**
     * 知识点4 函数重载：
     *  4.1
     *   同名函数可以有多个，并互为重载函数
     *   我们看着代码是两个函数名相同，但是编译器编译后，两个函数的名字并不相同，编译后的名字可能是这样的
     *      ?real@complex@@QBENXZ
     *      ?real@complex@@QAENABN@Z
     *   其实在对于一个程序来说，根本就不存在两个同名的函数，也不允许存在同名的函数。
     * @param r
     * @return
     */
    void real(double r) { re = r; }

    /**
     * 知识点6 const：
     *  const修饰的变量（对象）是不允许被修改的
     *  函数可以分为两种：改变内部数据的函数和不改变内部数据的函数，
     *  对于不改变内部数据的函数，一定不要忘记加const，如果不加const，就表示这个函数有可能会改变内部数据，在某些情况下会有问题：
     *    const complex c1(2, 1);
     *    c1.imag();    //  如果imag函数没有使用const修饰，此行将无法编译通过。
     *    从逻辑上讲，一个不可变的复数对象c1，获取自己的虚部，这是合理的一件事，因此imag函数必须加const修饰，才能合乎情理。
     *  总结：对于不改变内部数据的函数，应养成加const修饰的习惯，这是很容易被忽略的事情。
     * @return
     */
    double imag() const { return im; }

    /**
     * 知识点 9 friend 友元函数：
     *  9.2
     *   注：下面这个函数不是complex必有的函数，仅仅为了讲解这个知识点而添加的
     *  这个函数的含义是：使用一个对象，去处理相同class的另一个对象的数据
     *   使用：
     *     complex c1(2, 1);
     *     complex c2;
     *     c2.func(c1);
     *  问题：为什么这个对象可以直接使用另一个对象的私有成员。
     *  一个很好的解释是：相同class的各个objects互为friends(友元)
     * @param param
     * @return
     */
    int func(const complex &param)
    {
        return param.re + param.im;
    }

    /**
     * 知识点2 访问权限：
     *  2.2
     * complex c1(2, 1);
     * cout << c1.re;   // 私有成员无法在类外部使用，编译错误
     * cout << c1.im;   // 私有成员无法在类外部使用，编译错误
     */
private:
    double re, im;

    /**
     * 知识点5 把构造函数放在private中：
     *  传达什么含义呢？ 类对象不允许在类外部被创建。
     *   以下代码将报错
     *    complex c1(2, 1);
     *    complex c2;
     *  什么场景可以用到？ 单例模式(Singleton): 构造函数与拷贝构造函数设置为private
     */

    /**
     * 知识点 9 friend 友元函数：
     *  9.1
     *   被friend声明的函数，可以在该函数内使用该类的私有成员
     *   friend将会破坏类的封装性
     * @return
     */
    friend complex &__doapl(complex *, const complex &);
};

// 2 class之外的各种定义

double imag(const complex &x);

double real(const complex &x);

/**
 * 知识点10 操作符重载-2 非成员函数（无this）
 * 10.2
 *  用法：
 *    complex c1(2, 1);
 *    complex c2(5);
 *    c2 = c1 + c2;
 *    c2 = c1 + 3;
 *    c2 = 7 + c1;
 *  这3个函数为什么返回的不是引用类型？
 *   因为它们返回的必定是个local object. 返回的对象是在这个函数内部新创建的。
 *
 * 10.3
 * 任何一个操作都有两种写法：设计为成员函数和设计为全局函数，没有哪种方式效率更高，只能看选择哪种更合适，
 *                       并且同一个意思的函数，不能同时在成员函数和全局函数出现。
 *  选择哪种方式要看实际情况：
 *    1.成员函数优点：可以直接访问类私有成员
 *    2.全局函数：可以实现部分成员函数无法实现的逻辑：
 *       complex c1(2, 1);
 *       complex c2(5);
 *       c2 = c1 + c2; ==> 可以使用成员函数: complex operator+(const complex &y);
 *       c2 = c1 + 3; ==> 可以使用成员函数: complex operator+(double y);
 *       c2 = 7 + c1; 无法使用成员函数实现，只能通过全局函数实现。
 *      这也是成员函数的不足之处，操作符作用到的操作数只能是本类对象，不能是其他类型对象。
 *      就像 c2 = 7 + c1; 加号操作符作用的是int类型7，因此无法通过成员函数实现。
 *
 * 知识点11 temp object(临时对象) typename();
 *  类名() 是创建了一个临时对象（也叫匿名对象），这就好比定义一个int类型变量时这样写：
 *    int i = int(10);  int(10)产生的就是一个临时变量，然后再将这个临时变量赋值给i。
 *  下面的几种也都是临时对象：
 *    int(7);
 *    complex();
 *    complex(4, 5);
 *    cout << complex(2);
 *  临时对象的生命周期就仅仅是在这一行代码中。因此在return后使用是非常合适的。
 */
inline complex operator+(const complex &x, const complex &y)
{
    return complex(real(x) + real(y),
                   imag(x) + imag(y));
}

inline complex operator+(const complex &x, double y)
{
    return complex(real(x) + y, imag(x));
}

inline complex operator+(double x, const complex &y)
{
    return complex(x + real(y), imag(y));
}

/**
 * 10.4
 * 这个是取正操作，不是上面的加号操作
 * 用法：
 *  complex c1(2, 1);
 *  cout << +c1;
 *
 * 编译器是怎么区分加号操作和正号操作呢？
 *  看参数个数，只有一个参数肯定是正号操作。
 *
 * 这个函数内部并没有产生一个新的对象，是可以返回引用类型的，结果上是不会出错的。那为什么这里不返回引用类型呢？
 *  要知道：无论是正号操作还是负号操作，从逻辑上讲都是新产生了一个新的对象，因此不可以返回引用类型。
 *  如果返回引用类型（当然这里是可以返回引用类型的），接收者也是按引用类型接收的，比如：
 *   complex c1(5, 3);
 *   complex &c2 = +c1;
 *   当对c2的值修改时，c1的值也会相应的修改，这是不符合常理的（c1和c2应该互不影响才对），因此为避免这种情况发生，这里最好不要返回引用类型。
 * @param x
 * @return
 */
inline complex operator+(const complex &x)
{
    return x;
}

/**
 * 这个是取反操作，不是减号操作
 * 用法：
 *  complex c1(2, 1);
 *  cout << -c1;
 *
 * 上面的正号操作，写引用类型返回时，至少结果上不会出错，
 * 但本函数是在函数内部新创建了一个对象，因此绝不可以返回引用类型。
 * @param x
 * @return
 */
inline complex operator-(const complex &x)
{
    return complex(-real(x), -imag(x));
}

/**
 * ==操作符重载，原理等同+（加号）操作符重载
 * 用法：
 *  complex c1(2, 1);
 *  complex c2;
 *  cout << (c1 == c2);
 *  cout << (c1 == 2);
 *  cout << (0 == c2);
 * @param x
 * @param y
 * @return
 */
inline bool operator==(const complex &x, const complex &y)
{
    return real(x) == real(y) &&
           imag(x) == imag(y);
}

inline bool operator==(const complex &x, double y)
{
    return real(x) == y && imag(x) == 0;
}

inline bool operator==(double x, const complex &y)
{
    return x == real(y) && imag(y) == 0;
}

/**
 * !=操作符重载，原理等同==操作符重载
 * 用法：
 *  complex c1(2, 1);
 *  complex c2;
 *  cout << (c1 != c2);
 *  cout << (c1 != 2);
 *  cout << (0 != c2);
 * @param x
 * @param y
 * @return
 */
inline bool operator!=(const complex &x, const complex &y)
{
    return !(x == y);   // 可以利用==符号比较后，再取反，写法更简洁。
}

inline bool operator!=(const complex &x, double y)
{
    return !(x == y);
}

inline bool operator!=(double x, const complex &y)
{
    return !(x == y);
}

/**
 * 共轭复数：实部相等，虚部相反
 * 用法：
 *  complex c1(2, 1);
 *  complex c2 = conj(c1);
 * @param x
 * @return
 */
inline complex conj(const complex &x)
{
    return complex(real(x), -imag(x));
}

/**
 * 知识点10 操作符重载-2 非成员函数（无this）
 *  10.5
 *  用法：
 *   complex c1(c2, 1);
 *   complex c2;
 *   cout << c1 << c2;
 *  由于我们知道操作符一定是作用在左边变量身上的，针对这个函数 <<操作符是作用在ostream类型对象cout身上的,
 *  如果我们要将<<操作符重载写成成员函数，那么就要在ostream类中实现，然而ostream是标准库中实现的，我们无法修改它的代码，
 *  因此只能使用全局函数实现操作符<<的重载逻辑。
 * @param os
 * @param x
 * @return
 */
std::ostream &operator<<(std::ostream &os, const complex &x);


#endif //__COMPLEX_H__
